/*
 * jQuery JSONP Core Plugin 2.4.0 (2012-08-21)
 *
 * https://github.com/jaubourg/jquery-jsonp
 *
 * Copyright (c) 2012 Julian Aubourg
 *
 * This document is licensed as free software under the terms of the
 * MIT License: http://www.opensource.org/licenses/mit-license.php
 */
(function ($) {

    // ###################### UTILITIES ##

    // Noop
    function noop() {
    }

    // Generic callback
    function genericCallback(data) {
        lastValue = [data];
    }

    // Call if defined
    function callIfDefined(method, object, parameters) {
        return method && method.apply && method.apply(object.context || object, parameters);
    }

    // Give joining character given url
    function qMarkOrAmp(url) {
        return /\?/.test(url) ? "&" : "?";
    }

    var // String constants (for better minification)
        STR_ASYNC = "async",
        STR_CHARSET = "charset",
        STR_EMPTY = "",
        STR_ERROR = "error",
        STR_INSERT_BEFORE = "insertBefore",
        STR_JQUERY_JSONP = "_jqjsp",
        STR_ON = "on",
        STR_ON_CLICK = STR_ON + "click",
        STR_ON_ERROR = STR_ON + STR_ERROR,
        STR_ON_LOAD = STR_ON + "load",
        STR_ON_READY_STATE_CHANGE = STR_ON + "readystatechange",
        STR_READY_STATE = "readyState",
        STR_REMOVE_CHILD = "removeChild",
        STR_SCRIPT_TAG = "<script>",
        STR_SUCCESS = "success",
        STR_TIMEOUT = "timeout",

    // Window
        win = window,
    // Deferred
        Deferred = $.Deferred,
    // Head element
        head = $("head")[0] || document.documentElement,
    // Page cache
        pageCache = {},
    // Counter
        count = 0,
    // Last returned value
        lastValue,

    // ###################### DEFAULT OPTIONS ##
        xOptionsDefaults = {
            //beforeSend: undefined,
            //cache: false,
            callback: STR_JQUERY_JSONP,
            //callbackParameter: undefined,
            //charset: undefined,
            //complete: undefined,
            //context: undefined,
            //data: "",
            //dataFilter: undefined,
            //error: undefined,
            //pageCache: false,
            //success: undefined,
            //timeout: 0,
            //traditional: false,
            url: location.href
        },

    // opera demands sniffing :/
        opera = win.opera,

    // IE < 10
        oldIE = !!$("<div>").html("<!--[if IE]><i><![endif]-->").find("i").length;

    // ###################### MAIN FUNCTION ##
    function jsonp(xOptions) {

        // Build data with default
        xOptions = $.extend({}, xOptionsDefaults, xOptions);

        // References to xOptions members (for better minification)
        var successCallback = xOptions.success,
            errorCallback = xOptions.error,
            completeCallback = xOptions.complete,
            dataFilter = xOptions.dataFilter,
            callbackParameter = xOptions.callbackParameter,
            successCallbackName = xOptions.callback,
            cacheFlag = xOptions.cache,
            pageCacheFlag = xOptions.pageCache,
            charset = xOptions.charset,
            url = xOptions.url,
            data = xOptions.data,
            timeout = xOptions.timeout,
            pageCached,

        // Abort/done flag
            done = 0,

        // Life-cycle functions
            cleanUp = noop,

        // Support vars
            supportOnload,
            supportOnreadystatechange,

        // Request execution vars
            firstChild,
            script,
            scriptAfter,
            timeoutTimer;

        // If we have Deferreds:
        // - substitute callbacks
        // - promote xOptions to a promise
        Deferred && Deferred(function (defer) {
            defer.done(successCallback).fail(errorCallback);
            successCallback = defer.resolve;
            errorCallback = defer.reject;
        }).promise(xOptions);

        // Create the abort method
        xOptions.abort = function () {
            !( done++ ) && cleanUp();
        };

        // Call beforeSend if provided (early abort if false returned)
        if (callIfDefined(xOptions.beforeSend, xOptions, [xOptions]) === !1 || done) {
            return xOptions;
        }

        // Control entries
        url = url || STR_EMPTY;
        data = data ? ( (typeof data) == "string" ? data : $.param(data, xOptions.traditional) ) : STR_EMPTY;

        // Build final url
        url += data ? ( qMarkOrAmp(url) + data ) : STR_EMPTY;

        // Add callback parameter if provided as option
        callbackParameter && ( url += qMarkOrAmp(url) + encodeURIComponent(callbackParameter) + "=?" );

        // Add anticache parameter if needed
        !cacheFlag && !pageCacheFlag && ( url += qMarkOrAmp(url) + "_" + ( new Date() ).getTime() + "=" );

        // Replace last ? by callback parameter
        url = url.replace(/=\?(&|$)/, "=" + successCallbackName + "$1");

        // Success notifier
        function notifySuccess(json) {

            if (!( done++ )) {

                cleanUp();
                // Pagecache if needed
                pageCacheFlag && ( pageCache [url] = {s: [json]} );
                // Apply the data filter if provided
                dataFilter && ( json = dataFilter.apply(xOptions, [json]) );
                // Call success then complete
                callIfDefined(successCallback, xOptions, [json, STR_SUCCESS, xOptions]);
                callIfDefined(completeCallback, xOptions, [xOptions, STR_SUCCESS]);

            }
        }

        // Error notifier
        function notifyError(type) {

            if (!( done++ )) {

                // Clean up
                cleanUp();
                // If pure error (not timeout), cache if needed
                pageCacheFlag && type != STR_TIMEOUT && ( pageCache[url] = type );
                // Call error then complete
                callIfDefined(errorCallback, xOptions, [xOptions, type]);
                callIfDefined(completeCallback, xOptions, [xOptions, type]);

            }
        }

        // Check page cache
        if (pageCacheFlag && ( pageCached = pageCache[url] )) {

            pageCached.s ? notifySuccess(pageCached.s[0]) : notifyError(pageCached);

        } else {

            // Install the generic callback
            // (BEWARE: global namespace pollution ahoy)
            win[successCallbackName] = genericCallback;

            // Create the script tag
            script = $(STR_SCRIPT_TAG)[0];
            script.id = STR_JQUERY_JSONP + count++;

            // Set charset if provided
            if (charset) {
                script[STR_CHARSET] = charset;
            }

            opera && opera.version() < 11.60 ?
                // onerror is not supported: do not set as async and assume in-order execution.
                // Add a trailing script to emulate the event
                ( ( scriptAfter = $(STR_SCRIPT_TAG)[0] ).text = "document.getElementById('" + script.id + "')." + STR_ON_ERROR + "()" )
                :
                // onerror is supported: set the script as async to avoid requests blocking each others
                ( script[STR_ASYNC] = STR_ASYNC )

            ;

            // Internet Explorer: event/htmlFor trick
            if (oldIE) {
                script.htmlFor = script.id;
                script.event = STR_ON_CLICK;
            }

            // Attached event handlers
            script[STR_ON_LOAD] = script[STR_ON_ERROR] = script[STR_ON_READY_STATE_CHANGE] = function (result) {

                // Test readyState if it exists
                if (!script[STR_READY_STATE] || !/i/.test(script[STR_READY_STATE])) {

                    try {

                        script[STR_ON_CLICK] && script[STR_ON_CLICK]();

                    } catch (_) {
                    }

                    result = lastValue;
                    lastValue = 0;
                    result ? notifySuccess(result[0]) : notifyError(STR_ERROR);

                }
            };

            // Set source
            script.src = url;

            // Re-declare cleanUp function
            cleanUp = function (i) {
                timeoutTimer && clearTimeout(timeoutTimer);
                script[STR_ON_READY_STATE_CHANGE] = script[STR_ON_LOAD] = script[STR_ON_ERROR] = null;
                head[STR_REMOVE_CHILD](script);
                scriptAfter && head[STR_REMOVE_CHILD](scriptAfter);
            };

            // Append main script
            head[STR_INSERT_BEFORE](script, ( firstChild = head.firstChild ));

            // Append trailing script if needed
            scriptAfter && head[STR_INSERT_BEFORE](scriptAfter, firstChild);

            // If a timeout is needed, install it
            timeoutTimer = timeout > 0 && setTimeout(function () {
                    notifyError(STR_TIMEOUT);
                }, timeout);

        }

        return xOptions;
    }

    // ###################### SETUP FUNCTION ##
    jsonp.setup = function (xOptions) {
        $.extend(xOptionsDefaults, xOptions);
    };

    // ###################### INSTALL in jQuery ##
    $.jsonp = jsonp;

})(jQuery);
